package Schedule::Chronic::Distributed::Node;

=head1 NAME

Schedule::Chronic::Distributed::Node - Node

=head1 SYNOPSIS

use Schedule::Chronic::Distributed::Node;

.. TODO..

=head1 DESCRIPTION

Class representing individual nodes, or machines within the distributed scheduler.

=head1 METHODS

=head2 ACCESSORS

=over

=item id

=item name

=item description

=item available

=item last_seen

=item last_started

=item timedout

=item constraint_summary

=back

=head2 check_in

Object method to update the central database time that this node was last seen and active.

Requires a Log::Log4perl object to be passed as the only argument.

=head2 constraints_as_sql

Object method that returns the current system constraints on this node as an SQL snippet

=cut

use strict;
use Time::Piece;
use Module::Find;

use Schedule::Chronic::Distributed::Task;
our @constraint_classes = usesub Schedule::Chronic::Distributed::Constraint;

use base qw(Schedule::Chronic::Distributed::DBI);

Schedule::Chronic::Distributed::Node->table('nodes');
Schedule::Chronic::Distributed::Node->table_alias('node');
__PACKAGE__->sequence('node_id_seq');
Schedule::Chronic::Distributed::Node->columns(All => qw/id name description available last_seen last_started timedout constraint_summary/);
Schedule::Chronic::Distributed::Node->columns(Primary => 'id');

# Schedule::Chronic::Distributed::Node->has_many(last_node_for_tasks => 'Schedule::Chronic::Distributed::Task', 'last_node');
# Schedule::Chronic::Distributed::Node->has_many(current_node_for_tasks => 'Schedule::Chronic::Distributed::Task', 'current_node');

__PACKAGE__->has_a( last_seen => 'Time::Piece',
		    inflate => sub { return __PACKAGE__->inflate_timepiece(@_,'last_seen'); },
		    deflate => sub { return __PACKAGE__->deflate_timepiece(@_) } );

__PACKAGE__->has_a( last_started => 'Time::Piece',
		    inflate => sub { return __PACKAGE__->inflate_timepiece(@_,'last_started'); },
		    deflate => sub { return __PACKAGE__->deflate_timepiece(@_) } );

__PACKAGE__->set_sql ( catch_timedout_nodes => <<END_ORPHAN_TASKS_SQL, 0);
  UPDATE nodes
  SET available = 0, timedout = 1
  WHERE last_seen < (now() - interval '180 seconds')
END_ORPHAN_TASKS_SQL



sub check_in {
    my ($self,$logger) = @_;
    $logger->info("check in");
    if ($self->timedout or (!$self->available)) {
      $logger->warn("this self timedout") if ($self->timedout);
      # self has timed out/started
      # updates availability, checks for any jobs on this self
      $self->_started($logger);
    } else {
      # check in and check selfs/tasks
      $self->_update_lastseen;
      $self->_check_nodes_and_tasks($logger);
    }

    # TODO: generate summary_of_constraint_values;
    # my $summary_of_constraint_values = '';
    # $self->constraint_summary($summary_of_constraint_values);
    return;
}

sub constraints_as_sql {
  my $self = shift;
  my @constraint_conditions = ();
  foreach my $constraint (@{$self->_current_constraints}) {
    next unless $constraint->{class} =~ m!([a-z\d]+::)*[a-z\d]!i;
    push (@constraint_conditions, 'NOT( tr.parameters < ' . $constraint->{value} . ' AND tr.requirement = ' . $constraint->{req_id} . ')'  );
  }

  my $sql = 'CASE WHEN (' . join ("\nAND\n",@constraint_conditions) . ') THEN 1 ELSE 0 END ' ;

  return $sql;
}

#
#############################
# Private methods / functions

sub _update_lastseen {
  my $self = shift;
  my $t = localtime;
  $self->last_seen($t);
  $self->update();
  $self->db_Main->{CachedKids} = {};
}

sub _started {
  my ($self,$logger) = @_;
  my $t = localtime;
  foreach my $task (Schedule::Chronic::Distributed::Task->search(current_node=>$self->id, current_status=>'running') ) {
    $task->current_status('error');
    $task->error('failed to stop when scheduler shut down');
    $task->_update_history();
    # update current/next times
    $task->_update_times();
    $task->update();
    $logger->warn("task $task should have stopped when we last shut down but did not");
  }

  $self->timedout(0);
  $self->last_started($t);
  $self->last_seen($t);
  $self->available(1);
  $self->update();
}

sub _check_nodes_and_tasks {
  my ($self,$logger) = @_;

  # lock nodes table and begin transaction
  # before cleanup of orphaned tasks and timedout nodes
  my $dbh = $self->db_Main;
  my $rc = $dbh->begin_work or die $dbh->errstr;
  my $locked = $dbh->do('LOCK TABLE task IN ROW EXCLUSIVE MODE');

  # time out nodes where more than 180 seconds since last seen
  my $sth = $self->sql_catch_timedout_nodes;
  my $rv = $sth->execute();

  # mark running tasks as timed out where node is absent or timedout
  Schedule::Chronic::Distributed::Task->catch_orphaned_jobs;

  # commit and unlock
  $dbh->commit;
}


sub _current_constraints {
  my $self = shift;
  my $constraints = [];
  foreach my $constraint_class ( @constraint_classes ) {
    $constraint_class->initialise_class unless (defined $constraint_class->id);
    next unless ($constraint_class->type eq 'node');
    my $constraint = $constraint_class->new();
    push (@$constraints, { value => $constraint->update, req_id => $constraint_class->id, class=>$constraint_class, type=>$constraint_class->type} );
  }
  return $constraints;
}


1;

__END__

=head1 SEE ALSO

Schedule::Chronic::Distributed

=head1 AUTHOR

aaron trevena, E<lt>aaron.trevena@gmail.comE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2006 by Aaron Trevena

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.8.5 or,
at your option, any later version of Perl 5 you may have available.

=cut
